package top.swimmer.core.mybatis;

import org.apache.ibatis.binding.MapperMethod;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.reflection.ExceptionUtil;
import org.apache.ibatis.session.SqlSession;
import org.postgresql.util.PGobject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import top.swimmer.core.model.BaseModel;
import top.swimmer.core.model.result.Results;
import top.swimmer.core.session.Universe;
import top.swimmer.tools.Action;
import top.swimmer.tools.common.CloneUtils;
import top.swimmer.tools.common.Utils;

import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.SQLException;
import java.util.*;

import static top.swimmer.tools.common.Utils.guessComponentType;
import static top.swimmer.tools.common.Utils.toJSON;

public class MagicMapperProxy<T> implements InvocationHandler, Serializable {
    protected final Logger LOGGER = LoggerFactory.getLogger(this.getClass());

    private final SqlSession sqlSession;
    private final SqlSession slaveSqlSession;

    private final Class<T> mapperInterface;
    private final Map<Method, MagicMapperMethod> methodCache;

    private final Map<Method, Method> hackedMethodCache = new HashMap<>();

    public static <T> T newMapperProxy(Class<T> mapperInterface,
                                       SqlSession sqlSession,
                                       SqlSession slaveSqlSession,
                                       Map<Method, MapperMethod> methodCache) {
        ClassLoader classLoader = mapperInterface.getClassLoader();
        Class<?>[] interfaces = new Class[]{mapperInterface};

        MagicMapperProxy proxy = new MagicMapperProxy(
                mapperInterface,
                sqlSession,
                slaveSqlSession,
                methodCache);

        return (T) Proxy.newProxyInstance(classLoader, interfaces, proxy);
    }

    public MagicMapperProxy(Class<T> mapperInterface,
                            SqlSession sqlSession,
                            SqlSession slaveSqlSession,
                            Map<Method, MagicMapperMethod> methodCache) {
        this.sqlSession = sqlSession;
        this.slaveSqlSession = slaveSqlSession;

        this.mapperInterface = mapperInterface;
        this.methodCache = methodCache;
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        if (Object.class.equals(method.getDeclaringClass())) {
            try {
                return method.invoke(this, args);
            } catch (Throwable t) {
                throw ExceptionUtil.unwrapThrowable(t);
            }
        }

        if (method.getReturnType() == Results.class) {
            return processResultsReturn(proxy, method, args);
        }

        String methodName = method.getName();
        final MagicMapperMethod mapperMethod = cachedMapperMethod(method);

        SqlCommandType commandType = getCommandType(methodName, getSqlSession());

        // check slave query
        checkSlaveQuery(commandType);

        // pre-process model
        preHijackModel(args, commandType);

        Object result = mapperMethod.execute(getSqlSession(), args);

        // post-process model
        postHijackModel(args);

        if (result == null)
            return null;

        // deserialize single model
        if (BaseModel.class.isAssignableFrom(result.getClass())) {
            return buildResultModel(result);
        }

        // deserialize model list
        if (List.class.isAssignableFrom(result.getClass())) {
            Iterator iter = ((Collection) result).iterator();

            if (!iter.hasNext())
                return result;

            List<BaseModel> finalResults = new ArrayList<>();
            while (iter.hasNext()) {
                Object element = iter.next();

                if (element == null)
                    continue;

                if (!BaseModel.class.isAssignableFrom(element.getClass()))
                    return result;

                finalResults.add(buildResultModel(element));
            }

            return finalResults;
        }

        return result;
    }

    public Object processResultsReturn(Object proxy, Method method, Object[] args) throws Throwable {
        // initialize results
        Results results = new Results();

        try {
            Method hackedMethod = hackedMethodCache.get(method);

            if (hackedMethod == null) {
                hackedMethod = CloneUtils.clone(method);

                // hack mapper method return type
                Field returnTypeField = hackedMethod.getClass().getDeclaredField("returnType");
                returnTypeField.setAccessible(true);
                returnTypeField.set(hackedMethod, List.class);

                hackedMethodCache.put(method, hackedMethod);
            }

            // get result items
            results.setItems((List) invoke(proxy, hackedMethod, args));

            // get result total count

            // enter counting mode
            Universe.current().setCountingMode(true);

            Object result = invoke(proxy, hackedMethod, args);

            // use model id to to store total count for now
            if (((Iterable) result).iterator().hasNext()) {
                results.setTotal(((BaseModel) ((Iterable) result).iterator().next()).getId());
            } else {
                results.setTotal(0);
            }
        } finally {
            // exit counting mode
            Universe.current().setCountingMode(false);
        }

        return results;
    }

    private MagicMapperMethod cachedMapperMethod(Method method) {
        MagicMapperMethod mapperMethod = methodCache.get(method);
        if (mapperMethod == null) {
            mapperMethod = new MagicMapperMethod(mapperInterface, method, getSqlSession().getConfiguration());
            methodCache.put(method, mapperMethod);
        }
        return mapperMethod;
    }

    private void preHijackModel(Object args[], final SqlCommandType commandType) {
        processArguments(args, arg -> processModel((BaseModel) arg, commandType));
    }

    private void postHijackModel(Object args[]) {
        processArguments(args, arg -> ((BaseModel) arg).setPayload(null));
    }

    private void processModel(BaseModel model, SqlCommandType commandType) {
        // set default value
        setDefaultValue(model, commandType);

        // set payload
        model.setPayload(null); // erase payload in toJSON result

        try {
            PGobject payload = new PGobject();
            payload.setType("json");
            payload.setValue(toJSON(model));
            model.setPayload(payload);
        } catch (SQLException e) {

        }

        // for secured model
//        if (model instanceof SecuredModel && Universe.current().isSecuredCheckEnabled()) {
//            if (SqlCommandType.INSERT.equals(commandType) || SqlCommandType.UPDATE.equals(commandType)) {
//                SecuredModel securedModel = (SecuredModel) model;
//                Long salt = new SecureRandom().nextLong();
//                securedModel.setSalt(salt);
//
//                securedModel.setSign(genSecuredSign(securedModel));
//
//                // refresh payload with sign
//                securedModel.setPayload(toJSON(securedModel));
//            }
//        }
    }

    private void processArguments(Object args[], Action<Object> callback) {
        if (args == null)
            return;

        for (Object arg : args) {
            if (arg == null)
                continue;

            if (arg instanceof BaseModel) {
                callback.apply(arg);
            }

            if (arg instanceof List) {
                List listArg = (List) arg;

                Class<?> componentType = guessComponentType(listArg);
                if (componentType != null && BaseModel.class.isAssignableFrom(componentType)) {
                    for (Object element : listArg) {
                        callback.apply(element);
                    }
                }
            }
        }
    }

    private void setDefaultValue(BaseModel model, SqlCommandType commandType) {
        if (commandType == SqlCommandType.INSERT) {
            if (model.getCreatedTime() == null)
                model.setCreatedTime(Utils.now());

            if (model.getCreatedBy() == null)
                model.setCreatedBy(Universe.current().getUserName());
        }

        if (commandType == SqlCommandType.UPDATE || commandType == SqlCommandType.INSERT) {
            model.setUpdatedTime(Utils.now());
            model.setUpdatedBy(Universe.current().getUserName());
        }

        if (model.getOwner() == null) {
            model.setOwner(Universe.current().getUserId());
        }

        if (model.getOwnerType() == null) {
            model.setOwnerType(Universe.current().getUserType());
        }
    }

    private SqlCommandType getCommandType(String methodName, SqlSession session) {
        return session.getConfiguration().getMappedStatement(
                this.mapperInterface.getName() + "." + methodName).getSqlCommandType();
    }

    private BaseModel buildResultModel(Object result) {
        // special logic for counting mode
        if (Universe.current().isCountingMode())
            return (BaseModel) result;

        BaseModel finalResult = (BaseModel) (Utils.fromJson(((BaseModel) result).getPayload().getValue(), result.getClass()));

        finalResult.setId(((BaseModel) result).getId());
        finalResult.setOwner(((BaseModel) result).getOwner());
        finalResult.setOwnerType(((BaseModel) result).getOwnerType());

        finalResult.setCreatedBy(((BaseModel) result).getCreatedBy());
        finalResult.setCreatedTime(((BaseModel) result).getCreatedTime());

        finalResult.setUpdatedBy(((BaseModel) result).getUpdatedBy());
        finalResult.setUpdatedTime(((BaseModel) result).getUpdatedTime());

        // clear payload
        finalResult.setPayload(null);

        // check secured model sign
//        if (finalResult instanceof SecuredModel && Universe.current().isSecuredCheckEnabled()) {
//            SecuredModel securedFinalResult = (SecuredModel) finalResult;
//
//            String expectedSign = ModelUtils.genSecuredSign(securedFinalResult);
//            if (!Utils.equals(expectedSign, securedFinalResult.getSign())) {
//                throw new IllegalStateException("Invalid secured model sign for entity [" + finalResult.getId() + "].");
//            }
//        }

        return finalResult;
    }

    private SqlSession getSqlSession() {
        return Universe.current().isSlaveQuery() ? slaveSqlSession : sqlSession;
    }

    private void checkSlaveQuery(SqlCommandType commandType) {
        if (Universe.current().isSlaveQuery() && commandType != SqlCommandType.SELECT) {
            throw new IllegalStateException("Only [SELECT] query can be executed against slave database.");
        }
    }
}
